from Tkinter import *
import Image, ImageTk
from threading import Lock
from math import floor

# sample grid for testing purposes

GRID_ANS = [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
            [0,1,0,1,1,1,1,0,1,0,1,0,1,0,1]]


# global constants/variables

GRID_HEIGHT = len(GRID_ANS)
GRID_WIDTH = max([len(GRID_ANS[i]) for i in range(GRID_HEIGHT)])

HINT_WIDTH = int(floor((GRID_WIDTH+1)/2))
HINT_HEIGHT = int(floor((GRID_HEIGHT+1)/2))

# flip the grid around so that grid[x][y] actually gets the right element
def transpose(matrix, default_value=0):
   if not matrix: return []
   return map(lambda *row: [elem or default_value for elem in row], *matrix)
# print len(GRID_ANS)
GRID_ANS = transpose(GRID_ANS)
# print len(GRID_ANS)

# relevant keycodes and groups of keycodes
KEYCODE_UP = 38
KEYCODE_DOWN = 40
KEYCODE_LEFT = 37
KEYCODE_RIGHT = 39
KEYCODE_DIR = [KEYCODE_UP,KEYCODE_DOWN,KEYCODE_LEFT,KEYCODE_RIGHT]
KEYCODE_VER = [KEYCODE_UP,KEYCODE_DOWN]
KEYCODE_HOR = [KEYCODE_LEFT,KEYCODE_RIGHT]
KEYCODE_PAUSE = 13
KEYCODE_FILL = 88
KEYCODE_ELIM = 90
KEYCODE_CHG_ST = [KEYCODE_FILL,KEYCODE_ELIM]

# keeps track of which keys are pressed, not necessarily all
# keys that are down will be in this list
KEYS_PRESSED = []

# returns both the input direction and its opposite
DIR_PAIRS = {KEYCODE_LEFT : KEYCODE_HOR,
             KEYCODE_RIGHT: KEYCODE_HOR,
             KEYCODE_UP   : KEYCODE_VER,
             KEYCODE_DOWN : KEYCODE_VER}

# grid images enum
BLANK = 0
BLANK_H = 1
ELIM = 2
ELIM_H = 3
FILL = 4
FILL_H = 5
BLANKS = [BLANK, BLANK_H]
ELIMS = [ELIM, ELIM_H]
FILLS = [FILL, FILL_H]

# returns the highlighted version of any image
HIGHLIGHT = {BLANK   : BLANK_H,
             BLANK_H : BLANK_H,
             ELIM    : ELIM_H ,
             ELIM_H  : ELIM_H ,
             FILL    : FILL_H ,
             FILL_H  : FILL_H }

# flags used in movement
DIR_JUST_MOVED = 0
MOVE_JUST_PUSHED = False

# write modes
WRITE_NONE = 0
WRITE_BLANK = 1
WRITE_ELIM = 3
WRITE_FILL = 5

WRITE_MODE = WRITE_NONE

# puzzle modes
MODE_MARIO = 0 # user is notified if an block is filled incorrectly
MODE_WARIO = 1 # user receives no feedback until completion of puzzle

# keeps track of scheduled move events
MOVE_ID = 0

# total number of squares that are filled in
TOTAL_FILLS = 0

# number of fills in the solution
FINAL_FILLS = 0
for row in GRID_ANS:
    for cell in row:
        FINAL_FILLS += cell

# must be initialized before the tk variables
ROOT = Tk()

# delays
MOVE_DELAY = IntVar(value=20) # time between each move
MOVE_PAUSE = IntVar(value=100) # time after first move

ERROR_DELAY = IntVar(value=200) # time before incorrect fill to elim
ERROR_PAUSE = IntVar(value=300) # time before user regains control after incorrect

# options
PUZZLE_MODE = IntVar(value=MODE_MARIO)
PERSISTENT_FILLS = BooleanVar(value=True)

# lock to keep events from being called while
# other events are still being handled
EVENT_LOCK = Lock()
MOVE_LOCK = Lock()

# handles all key presses
def key_press_handler (event):
    EVENT_LOCK.acquire()
    global MOVE_JUST_PUSHED
    global WRITE_MODE
    if(event.keycode not in KEYS_PRESSED):
        KEYS_PRESSED.append(event.keycode)
        if event.keycode in KEYCODE_DIR:
            MOVE_JUST_PUSHED = True
            move()
        if event.keycode in KEYCODE_CHG_ST:
            if intersection(KEYCODE_CHG_ST, KEYS_PRESSED[:-1]):
                KEYS_PRESSED.pop()
                KEYS_PRESSED.insert(0,event.keycode)
            else:
                if event.keycode is KEYCODE_FILL:
                    if GRID_VIS[GX][GY] in FILLS:
                        WRITE_MODE = WRITE_BLANK
                    else:
                        WRITE_MODE = WRITE_FILL
                if event.keycode is KEYCODE_ELIM:
                    if GRID_VIS[GX][GY] in ELIMS:
                        WRITE_MODE = WRITE_BLANK
                    else:
                        WRITE_MODE = WRITE_ELIM
                reveal(GX,GY,WRITE_MODE)
    EVENT_LOCK.release()

def key_release_handler (event):
    EVENT_LOCK.acquire()
    temp_flag = False
    global WRITE_MODE
    if event.keycode in KEYS_PRESSED:
        if event.keycode in KEYCODE_CHG_ST:
            if event.keycode is intersection(KEYCODE_CHG_ST, KEYS_PRESSED)[-1]:
                WRITE_MODE = WRITE_NONE
        KEYS_PRESSED.remove(event.keycode)
    if event.keycode in KEYCODE_DIR:
        move()
    if event.keycode in KEYCODE_CHG_ST and WRITE_MODE is WRITE_NONE:
        if KEYCODE_FILL in KEYS_PRESSED:
            WRITE_MODE = WRITE_FILL
        elif KEYCODE_ELIM in KEYS_PRESSED:
            WRITE_MODE = WRITE_ELIM
    EVENT_LOCK.release()

# returns the intersection of list1 and list2, retaining the order of list2
def intersection(list1, list2):
		return [i for i in list2 if i in list1]
    # ret = []
    # for i in list2:
        # if i in list1: ret.append(i)
    # return ret

# returns the horizontal and vertical displacements given all dir keys pressed
def direc(dir_keys):
    keys = []
    for key in dir_keys[::-1]:
        if not intersection(DIR_PAIRS[key],keys): keys.append(key)
    return hor_ver(keys)

#utility function which returns hor and ver displacement given a list of dirs
def hor_ver(keys):
    ver = 0
    hor = 0
    if KEYCODE_UP in keys: ver = -1
    if KEYCODE_DOWN in keys: ver = 1
    if KEYCODE_LEFT in keys: hor = -1
    if KEYCODE_RIGHT in keys: hor = 1
    return hor, ver

def move (called_after_delay = False):
    global GX
    global GY
    global MOVE_ID
    global DIR_JUST_MOVED
    global MOVE_JUST_PUSHED
    MOVE_LOCK.acquire()
    dir_keys = intersection(KEYCODE_DIR, KEYS_PRESSED)
    if not dir_keys:
        ROOT.after_cancel(MOVE_ID)
        MOVE_LOCK.release()
        return
    move_horizontal = 0
    move_vertical = 0
    if MOVE_JUST_PUSHED:
        move_horizontal, move_vertical = hor_ver([dir_keys[-1]])
    else:
        move_horizontal, move_vertical = direc(dir_keys)
    if not called_after_delay:
        direction = move_horizontal + 3*move_vertical # unique for all combos
        if not MOVE_JUST_PUSHED:
            if DIR_JUST_MOVED is direction:
                MOVE_LOCK.release()
                return
            else:
                move_horizontal, move_vertical = hor_ver([dir_keys[-1]])
                direction = move_horizontal + 3*move_vertical
        DIR_JUST_MOVED = direction
    ROOT.after_cancel(MOVE_ID)
    set_img(GX,GY,GRID_VIS[GX][GY]-1)
    GX += move_horizontal
    GY += move_vertical
    if GY < 0: GY = 0
    if GY > GRID_HEIGHT-1: GY = GRID_HEIGHT-1
    if GX < 0: GX = 0
    if GX > GRID_WIDTH-1: GX = GRID_WIDTH-1
    if WRITE_MODE is WRITE_NONE: set_img(GX, GY, GRID_VIS[GX][GY]+1)
    else: reveal(GX, GY, WRITE_MODE)
    if called_after_delay:
        MOVE_ID = ROOT.after(MOVE_DELAY.get(),move,True)
    else:
        MOVE_ID = ROOT.after(MOVE_PAUSE.get(),move,True)
    MOVE_JUST_PUSHED = False
    MOVE_LOCK.release()

def reveal(x,y,img):
    global KEYS_PRESSED
    global WRITE_MODE
    global TOTAL_FILLS
    if GRID_VIS[x][y] not in FILLS and img in FILLS:
        TOTAL_FILLS += 1
    elif GRID_VIS[x][y] in FILLS and img not in FILLS:
        if PERSISTENT_FILLS.get() and PUZZLE_MODE.get() is MODE_MARIO:
            set_img(x,y,FILL_H)
            if KEYCODE_FILL in KEYS_PRESSED:
                WRITE_MODE = WRITE_FILL
            return
        TOTAL_FILLS -= 1
    set_img(x,y,img)
    if PUZZLE_MODE.get() is MODE_MARIO and img is FILL_H and GRID_ANS[x][y] is 0:
        TOTAL_FILLS -= 1
        ROOT.after_cancel(MOVE_ID)
        bind_keys(False)
        KEYS_PRESSED = []
        WRITE_MODE = WRITE_NONE
        ROOT.after(ERROR_DELAY.get(),set_img,x,y,ELIM_H)
        ROOT.after(ERROR_PAUSE.get(),bind_keys)
    if TOTAL_FILLS is FINAL_FILLS:
        # win = check_solution()
        if check_solution():
            print "You won!\nThat picture is correct!"
            ROOT.after_cancel(MOVE_ID)
            KEYS_PRESSED = []
            WRITE_MODE = WRITE_NONE
            bind_keys(False)

def set_img (x, y, img):
    GRID_VIS[x][y] = img
    GRID_LABELS[x][y].configure(image = GRID_IMGS[img])

def check_solution():
    for x in range(GRID_WIDTH):
        for y in range(GRID_HEIGHT):
            if GRID_ANS[x][y] is 1 and GRID_VIS[x][y] not in FILLS:
                return False
            if GRID_ANS[x][y] is 0 and GRID_VIS[x][y] in FILLS:
                return False
    return True

def calculate_hints():
    global HINT_COLS
    global HINT_ROWS
    # print len(GRID_ANS)
    # print len(GRID_ANS[0])
    for x in range(GRID_WIDTH):
        num = 0
        for y in range(GRID_HEIGHT):
            if GRID_ANS[x][y] is 1:
                num+=1
            elif num > 0:
                HINT_COLS[x].append(num)
                num = 0
        if num > 0:
            HINT_COLS[x].append(num)
        if len(HINT_COLS[x]) > 1 and HINT_COLS[x][0] is 0:
            HINT_COLS[x].pop(0)
    for y in range(GRID_HEIGHT):
        num = 0
        for x in range(GRID_WIDTH):
            if GRID_ANS[x][y] is 1:
                num+=1
            elif num > 0:
                HINT_ROWS[y].append(num)
                num = 0
        if num > 0:
            HINT_ROWS[y].append(num)
        if len(HINT_ROWS[y]) > 1 and HINT_ROWS[y][0] is 0:
            HINT_ROWS[y].pop(0)
    hint_labels_hide()
    hint_labels_show()

def hint_labels_hide():
    for x in range(GRID_WIDTH):
        for y in range(HINT_HEIGHT):
            HINT_COLS_LABELS[x][y].configure(image=GRID_IMGS[0])
    for y in range(GRID_HEIGHT):
        for x in range(HINT_WIDTH):
            HINT_ROWS_LABELS[y][x].configure(image=GRID_IMGS[0])

def hint_labels_show():
    for x in range(GRID_WIDTH):
        y = 0
        for cur in HINT_COLS[x][::-1]:
            HINT_COLS_LABELS[x][y].configure(image=DIGIT_IMGS[cur])
            y += 1
    for y in range(GRID_HEIGHT):
        x = 0
        for cur in HINT_ROWS[y][::-1]:
            HINT_ROWS_LABELS[y][x].configure(image=DIGIT_IMGS[cur])
            x += 1

def bind_keys(flag=True):
    if flag:
        ROOT.bind("<KeyPress>",key_press_handler)
        ROOT.bind("<KeyRelease>",key_release_handler)
    else:
        ROOT.unbind("<KeyPress>")
        ROOT.unbind("<KeyRelease>")
bind_keys()

# load digit imgs
def load_digit_imgs():
    global DIGIT_IMGS
    digit_image = []
    DIGIT_IMGS = []
    for i in range(31):
        digit_image.append(Image.open('images/%d.bmp'%i))
        DIGIT_IMGS.append(ImageTk.PhotoImage(digit_image[i]))
load_digit_imgs()

# load grid imgs
def load_grid_imgs():
    global GRID_IMGS
    grid_image = [
        Image.open('images/grid_blank.bmp'),
        Image.open('images/grid_blank_h.bmp'),
        Image.open('images/grid_elim.bmp'),
        Image.open('images/grid_elim_h.bmp'),
        Image.open('images/grid_full.bmp'),
        Image.open('images/grid_full_h.bmp')]
    GRID_IMGS = []
    for i in range(6):
        GRID_IMGS.append(ImageTk.PhotoImage(grid_image[i]))
load_grid_imgs()

# load test grid

GRID_FRAME = Frame(ROOT)
GX = 0
GY = 0

# set up the grid
GRID_VIS = []
GRID_LABELS = []
for x in range(GRID_WIDTH):
    GRID_VIS.append([])
    GRID_LABELS.append([])
    for y in range(GRID_HEIGHT):
        GRID_VIS[x].append(0)
        GRID_LABELS[x].append(Label(GRID_FRAME,image = GRID_IMGS[0]))
        GRID_LABELS[x][y].configure(width=10,height=10,borderwidth = 0)
        GRID_LABELS[x][y].place(x=HINT_WIDTH*9+x*9,y=HINT_HEIGHT*9+y*9)
set_img(0,0,BLANK_H)

# set up space for the hints
HINT_COLS = []
HINT_ROWS = []
HINT_COLS_LABELS = []
HINT_ROWS_LABELS = []
for x in range(GRID_WIDTH):
    HINT_COLS.append([0])
    HINT_COLS_LABELS.append([])
    for y in range(HINT_HEIGHT):
        HINT_COLS_LABELS[x].append(Label(GRID_FRAME,image = GRID_IMGS[0]))
        HINT_COLS_LABELS[x][y].configure(width=10,height=10,borderwidth=0)
        HINT_COLS_LABELS[x][y].place(x=HINT_WIDTH*9+x*9,y=(HINT_HEIGHT-y-1)*9)
    HINT_COLS_LABELS[x][0].configure(image=DIGIT_IMGS[0])
for y in range(GRID_HEIGHT):
    HINT_ROWS.append([0])
    HINT_ROWS_LABELS.append([])
    for x in range(HINT_WIDTH):
        HINT_ROWS_LABELS[y].append(Label(GRID_FRAME,image = GRID_IMGS[0]))
        HINT_ROWS_LABELS[y][x].configure(width=10,height=10,borderwidth=0)
        HINT_ROWS_LABELS[y][x].place(x=(HINT_WIDTH-x-1)*9,y=HINT_HEIGHT*9+y*9)
    HINT_ROWS_LABELS[y][0].configure(image=DIGIT_IMGS[0])

calculate_hints()

# set up the interface

# grid
GRID_FRAME.configure(width=(GRID_WIDTH+HINT_WIDTH)*9+1,height=(GRID_HEIGHT+HINT_HEIGHT)*9+1)
GRID_FRAME.pack(anchor=NW,side=TOP)

# move speed slider
cur_speed_frame=Frame(ROOT)
Label(cur_speed_frame, text = "Cursor speed:").pack(anchor=W,side=LEFT)
Scale(cur_speed_frame, from_=1, to=100, orient=HORIZONTAL, variable=MOVE_DELAY).pack(anchor=W,side=LEFT)
cur_speed_frame.pack(anchor=NW,side=TOP)

# move delay slider
cur_delay_frame=Frame(ROOT)
Label(cur_delay_frame, text = "Cursor delay:").pack(anchor=W,side=LEFT)
Scale(cur_delay_frame, from_=0, to=500, orient=HORIZONTAL, variable=MOVE_PAUSE).pack(anchor=W,side=LEFT)
cur_delay_frame.pack(anchor=NW,side=TOP)

# error delay slider
err_delay_frame=Frame(ROOT)
Label(err_delay_frame, text = "Error delay:").pack(anchor=W,side=LEFT)
Scale(err_delay_frame, from_=0, to=500, orient=HORIZONTAL, variable=ERROR_DELAY).pack(anchor=W,side=LEFT)
err_delay_frame.pack(anchor=NW,side=TOP)

# error pause slider
err_pause_frame=Frame(ROOT)
Label(err_pause_frame, text = "Error pause:").pack(anchor=W,side=LEFT)
Scale(err_pause_frame, from_=0, to=1000, orient=HORIZONTAL, variable=ERROR_PAUSE).pack(anchor=W,side=LEFT)
err_pause_frame.pack(anchor=NW,side=TOP)

# defined before the mode choice so that they can reference this checkbox
persistent_checkbox = Checkbutton(ROOT, variable = PERSISTENT_FILLS, text = "Persistent fills")

# mode choices
mode_frame=Frame(ROOT)
Label(mode_frame, text = "Mode:").pack(anchor=NW,side=LEFT)
mode_subframe=Frame(mode_frame)
Radiobutton(mode_subframe, value=MODE_MARIO, variable=PUZZLE_MODE, text="Mario", command=lambda: persistent_checkbox.configure(state=NORMAL)).pack(anchor=W,side=TOP)
Radiobutton(mode_subframe, value=MODE_WARIO, variable=PUZZLE_MODE, text="Wario", command=lambda: persistent_checkbox.configure(state=DISABLED)).pack(anchor=W,side=TOP)
mode_subframe.pack(anchor=W,side=LEFT)
mode_frame.pack(anchor=NW,side=TOP)

# fills are permanent in mario mode
persistent_checkbox.pack(anchor=NW,side=TOP)

ROOT.title("test grid")
ROOT.mainloop()
